﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace DP.RecallDeal.Interface.ObjectTransformer
{
    public class ObjectTransformerService : IObjectTransformerService
    {
        /// <summary>
        /// This property presents the map between source object's properties and distination object's properties
        /// 
        /// </summary>
        //public IList<KeyValuePair<string, string>> Mappings { get; set; }
        public IList<KeyValuePair<MappingMember, MappingMember>> Mappings { get; set; }

        public ObjectTransformerService(IList<KeyValuePair<MappingMember, MappingMember>> mappings)
        {
            this.Mappings = mappings;
        }

        public ObjectTransformerService()
        {

        }

        public IList<MappingException> MappingExceptions 
        {
            get { return mappingExceptions; }
        }

        IList<MappingException> mappingExceptions = new List<MappingException>();

        /// <summary>
        /// Base on Mapping properties, assign Source property values to Destination property values
        /// </summary>
        /// <param name="srcObject">Source object</param>
        /// <param name="targetObject">Target object</param>
        /// <returns>List of MappingException, contains </returns>
        public virtual void TransformObject(object srcObject, object targetObject) 
        {
            mappingExceptions.Clear();
            
            foreach (KeyValuePair<MappingMember, MappingMember> map in Mappings)
            {
                object sourceValue = null;
                if (map.Key.Type == MemberType.Property)
                    sourceValue = GetValueFromProperty(srcObject, map.Key.Name, map.Key.CaseSensitive);
                if (map.Key.Type == MemberType.Field)
                    sourceValue = GetValueFromField(srcObject, map.Key.Name, map.Key.CaseSensitive);

                if (sourceValue == null) continue;

                if (map.Value.Type == MemberType.Property)
                    FillObjectToProperty(map.Value.Name, targetObject, sourceValue, map.Value.CaseSensitive);
                if (map.Value.Type == MemberType.Field)
                    FillObjectToField(map.Value.Name, targetObject, sourceValue, map.Value.CaseSensitive);

            }
        }        

        /// <summary>
        /// This method is used to fill value to object's property in the chain
        /// </summary>
        /// <param name="propertiesChain">Target property will be filled by value. Would be able to have the form of Parent.Child.GrandChild</param>
        /// <param name="targetInstance">Object instance</param>
        /// <param name="sourceValue">Desirable value will be filled to target instance's property</param>
        public virtual void FillObjectToProperty(string propertiesChain, object targetInstance, object sourceValue, bool caseSensitive)
        {
            if (targetInstance == null || sourceValue == null || string.IsNullOrEmpty(propertiesChain))
                return;

            string[] properties = propertiesChain.Split('.');
            string property = properties[0];            

            try
            {
                PropertyInfo propertyInfo;
                if (caseSensitive)
                    propertyInfo = targetInstance.GetType().GetProperty(property);
                else
                    propertyInfo = targetInstance.GetType().GetProperty(property, BindingFlags.IgnoreCase);

                if (properties.Length > 1) //It has child property
                {
                    object propertyInstance = propertyInfo.GetValue(targetInstance, null);

                    if (propertyInstance == null)
                    {
                        propertyInstance = Activator.CreateInstance(propertyInfo.PropertyType);
                        CastAndSetValueToProperty(propertyInfo, targetInstance, propertyInstance);
                    }

                    //Remove first element in properties chain
                    properties = properties.Where((x, idx) => idx != 0).ToArray();

                    //Form left child properties into format Parent.Child.GrandChild
                    string leftProperties = string.Join(".", properties);

                    //Recursive call
                    FillObjectToProperty(leftProperties, propertyInstance, sourceValue, caseSensitive);
                }
                else //No more child property
                    CastAndSetValueToProperty(propertyInfo, targetInstance, sourceValue);
            }
            catch (InvalidCastException castException)
            {
                mappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while sets value to target property: " + property,
                    InnerException = castException
                });
            }
            catch (ArgumentException argumentException)
            {
                mappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while sets value to target property: " + property,
                    InnerException = argumentException
                });
            }
            catch (NullReferenceException nullException)
            {
                mappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while sets value to target property: " + property,
                    InnerException = nullException
                });
            }
            catch (Exception ex)
            {
                mappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while sets value to target property: " + property,
                    InnerException = ex
                });
            }
        }

        public virtual void FillObjectToField(string fieldsChain, object targetInstance, object sourceValue, bool caseSensitive)
        {
            if (targetInstance == null || sourceValue == null || string.IsNullOrEmpty(fieldsChain))
                return;

            string[] fields = fieldsChain.Split('.');
            string field = fields[0];

            try
            {

                FieldInfo fieldInfo;
                
                if (caseSensitive)
                    fieldInfo = targetInstance.GetType().GetField(field);
                else
                    fieldInfo = targetInstance.GetType().GetField(field, BindingFlags.IgnoreCase);

                if (fields.Length > 1) //It has child field
                {
                    object fieldInstance = fieldInfo.GetValue(targetInstance);

                    if (fieldInstance == null)
                    {
                        fieldInstance = Activator.CreateInstance(fieldInfo.FieldType);
                        CastAndSetValueToField(fieldInfo, targetInstance, fieldInstance);
                    }

                    //Remove first element in fields chain
                    fields = fields.Where((x, idx) => idx != 0).ToArray();

                    //Form left child fields into format Parent.Child.GrandChild
                    string leftfields = string.Join(".", fields);

                    //Recursive call
                    FillObjectToField(leftfields, fieldInstance, sourceValue, caseSensitive);
                }
                else //No more child field
                    CastAndSetValueToField(fieldInfo, targetInstance, sourceValue);
            }
            catch (InvalidCastException castException)
            {
                MappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while sets value to target field: " + field,
                    InnerException = castException
                });
            }
            catch (ArgumentException argumentException)
            {
                MappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while sets value to target field: " + field,
                    InnerException = argumentException
                });
            }
            catch (NullReferenceException nullException)
            {
                MappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while sets value to target field: " + field,
                    InnerException = nullException
                });
            }
            catch (Exception ex)
            {
                MappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while sets value to target field: " + field,
                    InnerException = ex
                });
            }
        }

        /// <summary>
        /// This method is used to get value of an instance's property
        /// </summary>
        /// <param name="instance">Object instance</param>
        /// <param name="propertyName">Property which value will be taken. Would be able to have the form of "Parent.Child.GrandChild"</param>
        /// <param name="caseSensitive">Is property name compared with case sensitive perspective</param>
        /// <returns>Desirable value</returns>
        public virtual object GetValueFromProperty(object instance, string propertiesChain, bool caseSensitive)
        {
            if (instance == null && string.IsNullOrEmpty(propertiesChain))
                return null;

            string[] properties = propertiesChain.Split('.');
            string property = properties[0];

            try
            {
                PropertyInfo propertyInfo;
                if (caseSensitive)
                    propertyInfo = instance.GetType().GetProperty(property);
                else
                    propertyInfo = instance.GetType().GetProperty(property, BindingFlags.IgnoreCase);


                object propertyInstance = propertyInfo.GetValue(instance, null);

                if (properties.Length > 1) //It has child property
                {
                    //Remove first element in properties chain
                    properties = properties.Where((x, idx) => idx != 0).ToArray();

                    //Form left child properties into format Parent.Child.GrandChild
                    string leftProperties = string.Join(".", properties);

                    //Recursive call
                    return GetValueFromProperty(propertyInstance, leftProperties, caseSensitive);
                }
                else //No more child property
                    return propertyInstance;
            }
            catch (Exception ex)
            {
                mappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while gets value from source property: " + property,
                    InnerException = ex
                });
            }
            return null;                               
        }

        public virtual object GetValueFromField(object instance, string fieldsChain, bool caseSensitive)
        {
            if (instance == null && string.IsNullOrEmpty(fieldsChain))
                return null;

            string[] fields = fieldsChain.Split('.');
            string field = fields[0];

            try
            {
                FieldInfo fieldInfo = null;

                if (caseSensitive)
                    fieldInfo = instance.GetType().GetField(field);
                else
                    fieldInfo = instance.GetType().GetField(field, BindingFlags.IgnoreCase);

                object fieldInstance = fieldInfo.GetValue(instance);

                if (fields.Length > 1) //It has child property
                {
                    //Remove first element in properties chain
                    fields = fields.Where((x, idx) => idx != 0).ToArray();

                    //Form left child properties into format Parent.Child.GrandChild
                    string leftfields = string.Join(".", fields);

                    //Recursive call
                    return GetValueFromField(fieldInstance, leftfields, caseSensitive);
                }
                else //No more child property
                    return fieldInstance;
            }
            catch (Exception ex)
            {
                mappingExceptions.Add(new MappingException
                {
                    Message = "Mapping exception occurs while gets value from source field: " + field,
                    InnerException = ex
                });
            }
            return null;     
        }

        /// <summary>
        /// Cast and set value to target property
        /// </summary>
        /// <param name="info"></param>
        /// <param name="instance"></param>
        /// <param name="value"></param>
        protected void CastAndSetValueToProperty(PropertyInfo info, object instance, object value)
        {
            var destType = info.PropertyType.IsNullableType() ? Nullable.GetUnderlyingType(info.PropertyType) : info.PropertyType;
            var convertedValue = Convert.ChangeType(value, destType);

            info.SetValue(instance, convertedValue, null);
        }

        protected void CastAndSetValueToField(FieldInfo info, object instance, object value)
        {
            var destType = info.FieldType.IsNullableType() ? Nullable.GetUnderlyingType(info.FieldType) : info.FieldType;
            var convertedValue = Convert.ChangeType(value, destType);

            info.SetValue(instance, convertedValue);
        }
    }
}
